package org.jdns.jfreechess.server;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.List;

import javax.inject.Qualifier;

import org.jdns.jfreechess.server.commands.Command;
import org.jdns.jfreechess.server.commands.CommandMethodException;

import com.google.common.collect.Lists;
import com.google.inject.BindingAnnotation;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Provider;

/**
 * This class represents the method that is used to implement a command. Commands are implemented
 * by:
 * 
 * <ul>
 * <li>creating an implementation of the {@link Command} interface
 * <li>annotating the implementing class with the {@link CommandName} annotation
 * <li>annotating exactly one method with the {@link CommandHandler} annotation
 * </ul>
 * 
 * The {@code CommandMethod} class is responsible for executing the method described above. When the
 * user's command is received, the command method will be invoked in the {@link CommandScoped} Guice
 * scope.
 */
class CommandMethod {
  private final Class<? extends Command> commandClass;

  // on the first call to invoke, this list will be populated with one Provider for each argument of
  // the underlying method's parameters.
  private final List<Provider<?>> argumentProviders = Lists.newArrayList();
  private final Method underlyingMethod;
  private final Provider<Injector> injector;

  private volatile boolean initialized = false;

  CommandMethod(Provider<Injector> injector, Class<? extends Command> commandClass,
      Method underlyingMethod) {
    this.injector = injector;
    this.commandClass = commandClass;
    this.underlyingMethod = underlyingMethod;
  }

  private Annotation getBindingAnnotation(Annotation[] parameterAnnotations) {
    Annotation result = null;

    for (Annotation annotation : parameterAnnotations) {
      if (annotation.annotationType().isAnnotationPresent(BindingAnnotation.class)
          || annotation.annotationType().isAnnotationPresent(Qualifier.class)) {
        if (result != null) {
          throw new IllegalStateException("method has more than one binding annotation");
        }

        result = annotation;
      }
    }

    return result;
  }

  /**
   * Invokes the command method by first resolving any method parameters using Guice and then
   * invoking the method using reflection.
   */
  public Object invoke() throws CommandMethodException {
    Command command = initializeProvidersAndGetInjector().getInstance(commandClass);

    Object[] values = new Object[argumentProviders.size()];
    for (int i = 0; i < values.length; i++) {
      values[i] = argumentProviders.get(i).get();
    }

    try {
      return underlyingMethod.invoke(command, values);
    } catch (IllegalArgumentException e) {
      throw new CommandMethodException(e);
    } catch (IllegalAccessException e) {
      throw new CommandMethodException(e);
    } catch (InvocationTargetException e) {
      throw new CommandMethodException(e);
    }
  }

  private synchronized Injector initializeProvidersAndGetInjector() {
    if (!initialized) {
      Type[] parameterTypes = underlyingMethod.getGenericParameterTypes();
      Annotation[][] parameterAnnotations = underlyingMethod.getParameterAnnotations();

      for (int i = 0; i < parameterTypes.length; i++) {
        Annotation annotation = getBindingAnnotation(parameterAnnotations[i]);
        Key<?> key;

        if (annotation != null) {
          key = Key.get(parameterTypes[i], annotation);
        } else {
          key = Key.get(parameterTypes[i]);
        }

        argumentProviders.add(injector.get().getProvider(key));
        initialized = true;
      }
    }

    return injector.get();
  }
}